#!/usr/bin/env node

import axios, { AxiosRequestConfig } from 'axios';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

/**
 * Configure your Jira instance credentials and URL.
 */
const JIRA_URL = process.env.JIRA_URL;
const JIRA_API_MAIL = process.env.JIRA_API_MAIL;
const JIRA_API_KEY = process.env.JIRA_API_KEY;

/**
 * Create an MCP server to handle JQL queries.
 */
const server = new Server(
  {
    name: 'Jira communication server',
    version: '0.1.0',
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

/**
 * Handler for listing available tools.
 * Provides a tool to execute a JQL query against Jira.
 */
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'execute_jql',
        description:
          'Execute a JQL query on Jira on the api /rest/api/3/search/jql. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            jql: {
              type: 'string',
              description: 'JQL query string',
            },
            number_of_results: {
              type: 'integer',
              description: 'Number of results to return',
              default: 1,
            },
          },
          required: ['jql'],
        },
      },
      //as the previous tool gets everything in the ticket, we can create a new tool to get only the ticket name and description to fit more in the context of the assistant
      {
        name: 'get_only_ticket_name_and_description',
        description:
          'Get the name and description of the requested tickets on the api /rest/api/3/search/jql. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            jql: {
              type: 'string',
              description: 'JQL query string',
            },
            number_of_results: {
              type: 'integer',
              description: 'Number of results to return',
              default: 1,
            },
          },
          required: ['jql'],
        },
      },
      {
        name: 'create_ticket',
        description:
          'Create a ticket on Jira on the api /rest/api/3/issue. Do not use markdown in any field.',
        inputSchema: {
          type: 'object',
          properties: {
            project: {
              type: 'object',
              properties: {
                key: {
                  type: 'string',
                  description: 'The project key',
                },
              },
              required: ['key'],
            },
            summary: {
              type: 'string',
              description: 'The summary of the ticket',
            },
            description: {
              type: 'string',
              description: 'The description of the ticket',
            },
            issuetype: {
              type: 'object',
              properties: {
                name: {
                  type: 'string',
                  description: 'The name of the issue type',
                },
              },
              required: ['name'],
            },
            parent: {
              type: 'string',
              description: 'The key of the parent ticket (the epic)',
            },
          },
          required: ['project', 'summary', 'description', 'issuetype'],
        },
      },
      //liste les projets
      {
        name: 'list_projects',
        description:
          'List all the projects on Jira on the api /rest/api/3/project. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            number_of_results: {
              type: 'integer',
              description: 'Number of results to return',
              default: 1,
            },
          },
        },
      },
      //delete a ticket
      {
        name: 'delete_ticket',
        description:
          'Delete a ticket on Jira on the api /rest/api/3/issue/{issueIdOrKey}. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            issueIdOrKey: {
              type: 'string',
              description: 'The issue id or key',
            },
          },
          required: ['issueIdOrKey'],
        },
      },
      //edit ticket : name, description, assignee, priority, labels, components, custom fields
      {
        name: 'edit_ticket',
        description:
          'Edit a ticket on Jira on the api /rest/api/3/issue/{issueIdOrKey}. Do not use markdown in any field.',
        inputSchema: {
          type: 'object',
          properties: {
            issueIdOrKey: {
              type: 'string',
              description: 'The issue id or key',
            },
            summary: {
              type: 'string',
              description: 'The summary of the ticket',
            },
            description: {
              type: 'string',
              description: 'The description of the ticket',
            },
            labels: {
              type: 'array',
              items: {
                type: 'string',
              },
              description: 'The labels of the ticket',
            },
            parent: {
              type: 'string',
              description: 'The key of the parent ticket (the epic)',
            },
          },
          required: ['issueIdOrKey'],
        },
      },
      //get all status
      {
        name: 'get_all_statuses',
        description:
          'Get all the status on Jira on the api /rest/api/3/status. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            number_of_results: {
              type: 'integer',
              description: 'Number of results to return',
              default: 1,
            },
          },
        },
      },
      //assign ticket
      {
        name: 'assign_ticket',
        description:
          'Assign a ticket on Jira on the api /rest/api/3/issue/{issueIdOrKey}/assignee. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            accountId: {
              type: 'string',
              description: 'The account id of the assignee',
            },
            issueIdOrKey: {
              type: 'string',
              description: 'The issue id or key',
            },
          },
          required: ['accountId', 'issueIdOrKey'],
        },
      },
      //query assignables to ticket
      {
        name: 'query_assignable',
        description:
          'Query assignables to a ticket on Jira on the api /rest/api/3/user/assignable/search?project={project-name}. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            project_key: {
              type: 'string',
              description: 'The id of the project to search',
            },
          },
          required: ['project_key'],
        },
      },
      {
        name: 'add_attachment_from_public_url',
        description:
          'Add an attachment from a public url to a ticket on Jira on the api /rest/api/3/issue/{issueIdOrKey}/attachments. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            issueIdOrKey: {
              type: 'string',
              description: 'The issue id or key',
            },
            imageUrl: {
              type: 'string',
              description: 'The URL of the image to attach',
            },
          },
          required: ['issueIdOrKey', 'imageUrl'],
        },
      },
      {
        name: 'add_attachment_from_confluence',
        description:
          'Add an attachment to a ticket on Jira from a Confluence page by its name on the api /rest/api/3/issue/{issueIdOrKey}/attachments. Do not use markdown in your query.',
        inputSchema: {
          type: 'object',
          properties: {
            issueIdOrKey: {
              type: 'string',
              description: 'The issue id or key',
            },
            pageId: {
              type: 'string',
              description: 'The page id',
            },
            attachmentName: {
              type: 'string',
              description: 'The name of the attachment',
            },
          },
          required: ['issueIdOrKey', 'pageId', 'attachmentName'],
        },
      },
    ],
  };
});

/**
 * Function to add an attachment to a Jira issue from a Confluence page.
 * @param issueIdOrKey
 * @param pageId
 * @param attachmentName
 * @returns {Promise<any>}
 */
async function addAttachmentFromConfluence(
  issueIdOrKey: string,
  pageId: string,
  attachmentName: string,
): Promise<any> {
  try {
    // Récupérer l'attachement depuis Confluence
    const response = await axios.get(
      `${JIRA_URL}/wiki/rest/api/content/${pageId}/child/attachment`,
      {
        headers: getAuthHeaders().headers,
      },
    );

    // Trouver l'attachement spécifique
    const attachment = response.data.results.find(
      (attachment: any) => attachment.title === attachmentName,
    );

    if (!attachment) {
      return {
        error: 'Attachment not found',
      };
    }

    // Télécharger l'attachement
    const attachmentResponse = await axios.get(
      `${JIRA_URL}/wiki${attachment._links.download}`,
      {
        headers: getAuthHeaders().headers,
        responseType: 'arraybuffer',
      },
    );

    // Créer un FormData et ajouter le fichier
    const formData = new FormData();
    const blob = new Blob([attachmentResponse.data], {
      type: attachment.mediaType,
    });
    formData.append('file', blob, attachmentName);

    // Headers spéciaux pour l'upload de fichiers
    const headers = {
      ...getAuthHeaders().headers,
      'X-Atlassian-Token': 'no-check',
      'Content-Type': 'multipart/form-data',
    };

    // Uploader l'attachement sur le ticket Jira
    const uploadResponse = await axios.post(
      `${JIRA_URL}/rest/api/3/issue/${issueIdOrKey}/attachments`,
      formData,
      { headers },
    );

    return uploadResponse.data;
  } catch (error: any) {
    return {
      error: error.response?.data || error.message,
    };
  }
}

/**
 * Function to add an attachment to a Jira issue.
 * @param issueIdOrKey
 * @param imageUrl
 * @returns {Promise<any>}
 */
async function addAttachment(
  issueIdOrKey: string,
  imageUrl: string,
): Promise<any> {
  try {
    // Télécharger l'image depuis l'URL
    const imageResponse = await axios.get(imageUrl, {
      responseType: 'arraybuffer',
    });
    const formData = new FormData();
    formData.append('file', new Blob([imageResponse.data]), 'image.png');

    // Headers spéciaux pour l'upload de fichiers
    const headers = {
      ...getAuthHeaders().headers,
      'X-Atlassian-Token': 'no-check',
      'Content-Type': 'multipart/form-data',
    };

    const response = await axios.post(
      `${JIRA_URL}/rest/api/3/issue/${issueIdOrKey}/attachments`,
      formData,
      { headers },
    );

    return response.data;
  } catch (error: any) {
    return {
      error: error.response?.data || error.message,
    };
  }
}

/**
 * Function to query assignable users for a project.
 * @param {string} project_key - The project key to query assignable users for.
 * @returns {Promise<any>}
 */
async function queryAssignable(project_key: string): Promise<any> {
  try {
    const params = {
      project: project_key, // JQL query string
    };

    const response = await axios.get(
      `${JIRA_URL}/rest/api/3/user/assignable/search`,
      {
        headers: getAuthHeaders().headers,
        params,
      },
    );

    return response.data;
  } catch (error: any) {
    //return the error in a json
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to execute a JQL query against Jira.
 * @param {string} jql - JQL query string
 * @param maxResults
 * @returns {Promise<any>}
 */
async function executeJQL(jql: string, maxResults: number): Promise<any> {
  try {
    const params = {
      jql, // JQL query string
      maxResults, // Adjust as needed
      fields: '*all', // Request all fields
    };

    const response = await axios.get(`${JIRA_URL}/rest/api/3/search/jql`, {
      headers: getAuthHeaders().headers,
      params,
    });

    return response.data;
  } catch (error: any) {
    //return the error in a json
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to create a ticket on Jira.
 * @param project
 * @param summary
 * @param description
 * @param issuetype
 * @param parentID
 */
async function createTicket(
  project: string,
  summary: string,
  description: string,
  issuetype: string,
  parentID?: string,
): Promise<any> {
  try {
    const jiraDescription = {
      type: 'doc',
      version: 1,
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: description,
            },
          ],
        },
      ],
    };

    //parent is somethng like "parent": {"key": "SCRUM-19"}
    const parent = parentID ? { key: parentID } : undefined;

    const response = await axios.post(
      `${JIRA_URL}/rest/api/3/issue`,
      {
        fields: {
          project: {
            key: project,
          },
          summary,
          description: description ? jiraDescription : undefined,
          issuetype: {
            name: issuetype,
          },
          parent,
        },
      },
      {
        headers: getAuthHeaders().headers,
      },
    );

    return response.data;
  } catch (error: any) {
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to get the authentication headers.
 * @returns {AxiosRequestConfig}
 */
function getAuthHeaders(): AxiosRequestConfig<any> {
  const authHeader = `Basic ${Buffer.from(
    `${JIRA_API_MAIL}:${JIRA_API_KEY}`,
  ).toString('base64')}`;
  return {
    headers: {
      Authorization: authHeader,
      'Content-Type': 'application/json',
    },
  };
}

/** Function to list all projects on Jira.
 * @returns {Promise<any>}
 * @param number_of_results
 */
async function listProjects(number_of_results: number): Promise<any> {
  try {
    const params = {
      maxResults: number_of_results, // Adjust as needed
    };

    const response = await axios.get(`${JIRA_URL}/rest/api/3/project`, {
      headers: getAuthHeaders().headers,
      params,
    });

    return response.data;
  } catch (error: any) {
    //return the error in a json
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to delete a ticket on Jira.
 * @param issueIdOrKey
 * @returns {Promise<any>}
 */
async function deleteTicket(issueIdOrKey: string): Promise<any> {
  try {
    const response = await axios.delete(
      `${JIRA_URL}/rest/api/3/issue/${issueIdOrKey}`,
      {
        headers: getAuthHeaders().headers,
      },
    );
    return response.data;
  } catch (error: any) {
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to edit a ticket on Jira.
 * @param issueIdOrKey
 * @param summary
 * @param description
 * @param labels
 * @param parent
 * @returns {Promise<any>}
 */
async function editTicket(
  issueIdOrKey?: string,
  summary?: string,
  description?: string,
  labels?: string[],
  parent?: string,
): Promise<any> {
  try {
    const descriptionToSend = description || 'No description provided';

    const jiraDescription =
      description === null
        ? undefined
        : {
            type: 'doc',
            version: 1,
            content: [
              {
                type: 'paragraph',
                content: [
                  {
                    type: 'text',
                    text: descriptionToSend,
                  },
                ],
              },
            ],
          };

    const parentToSend = parent ? { key: parent } : undefined;

    //we create the fields object with only the present fields
    let fields: any = {
      summary: summary,
      labels: labels,
      parent: parentToSend,
    };

    if (description) {
      fields['description'] = jiraDescription;
    }

    const response = await axios.put(
      `${JIRA_URL}/rest/api/3/issue/${issueIdOrKey}`,
      {
        fields: fields,
      },
      {
        headers: getAuthHeaders().headers,
      },
    );

    return response.data;
  } catch (error: any) {
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to get all the statuses on Jira.
 * @param number_of_results
 * @returns {Promise<any>}
 */
async function getAllStatus(number_of_results: number): Promise<any> {
  try {
    const params = {
      maxResults: number_of_results, // Adjust as needed
    };

    const response = await axios.get(`${JIRA_URL}/rest/api/3/status`, {
      headers: getAuthHeaders().headers,
      params,
    });

    return response.data;
  } catch (error: any) {
    //return the error in a json
    return {
      error: error.response.data,
    };
  }
}

/**
 * Function to assign a ticket to a user.
 * @param accountId
 * @param issueIdOrKey
 * @returns {Promise<any>}
 */
async function assignTicket(
  accountId: string,
  issueIdOrKey: string,
): Promise<any> {
  try {
    const response = await axios.put(
      `${JIRA_URL}/rest/api/3/issue/${issueIdOrKey}/assignee`,
      {
        accountId,
      },
      {
        headers: getAuthHeaders().headers,
      },
    );

    return response.data;
  } catch (error: any) {
    return {
      error: error.response.data,
    };
  }
}

/**
 * Handler for the execute_jql tool.
 * Executes a JQL query and returns the full response.
 */
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case 'execute_jql': {
      const jql = String(request.params.arguments?.jql);
      const number_of_results = Number(
        request.params.arguments?.number_of_results ?? 1,
      );

      if (!jql) {
        throw new Error('JQL query is required');
      }

      const response = await executeJQL(jql, number_of_results);

      // Return the entire data from the response
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2), // Pretty print JSON
          },
        ],
      };
    }

    case 'get_only_ticket_name_and_description': {
      const jql = String(request.params.arguments?.jql);
      const number_of_results = Number(
        request.params.arguments?.number_of_results ?? 1,
      );

      if (!jql) {
        throw new Error('JQL query is required');
      }

      const response = await executeJQL(jql, number_of_results);

      // Return only the ticket name and description
      const tickets = response.issues.map((issue: any) => {
        return {
          key: issue.key,
          summary: issue.fields.summary,
          description: issue.fields.description,
        };
      });

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(tickets, null, 2), // Pretty print JSON
          },
        ],
      };
    }

    case 'create_ticket': {
      const project: any = request.params.arguments?.project;
      const summary: any = request.params.arguments?.summary;
      const description: any = request.params.arguments?.description;
      const issuetype: any = request.params.arguments?.issuetype;
      const parent: any = request.params.arguments?.parent;

      if (!project || !summary || !description || !issuetype) {
        throw new Error(
          'Project, summary, description and issuetype are required',
        );
      }

      try {
        const response = await createTicket(
          project.key,
          summary,
          description,
          issuetype.name,
          parent,
        );

        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(response, null, 2),
            },
          ],
        };
      } catch (error: any) {
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(error.response.data, null, 2),
            },
          ],
        };
      }
    }

    case 'list_projects': {
      const number_of_results = Number(
        request.params.arguments?.number_of_results ?? 1,
      );

      const response = await listProjects(number_of_results);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'delete_ticket': {
      const issueIdOrKey: any = request.params.arguments?.issueIdOrKey;

      if (!issueIdOrKey) {
        throw new Error('Issue id or key is required');
      }

      const response = await deleteTicket(issueIdOrKey);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'edit_ticket':
      const issueIdOrKey: any = request.params.arguments?.issueIdOrKey;
      const summary: any = request.params.arguments?.summary;
      const description: any = request.params.arguments?.description;
      const labels: any = request.params.arguments?.labels;
      const parent: any = request.params.arguments?.parent;

      if (!issueIdOrKey) {
        throw new Error('Issue id or key is required');
      }

      const response = await editTicket(
        issueIdOrKey,
        summary,
        description,
        labels,
        parent,
      );

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };

    case 'get_all_statuses': {
      const number_of_results = Number(
        request.params.arguments?.number_of_results ?? 50,
      );

      const response = await getAllStatus(number_of_results);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'assign_ticket': {
      const accountId: any = request.params.arguments?.accountId;
      const issueIdOrKey: any = request.params.arguments?.issueIdOrKey;

      if (!accountId || !issueIdOrKey) {
        throw new Error('Account id and issue id or key are required');
      }

      const response = await assignTicket(accountId, issueIdOrKey);

      return {
        content: [
          {
            type: 'text',
            text: 'Ticket assigned : ' + JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'query_assignable': {
      const project_key: any = request.params.arguments?.project_key;

      if (!project_key) {
        throw new Error('Query is required');
      }

      const response = await queryAssignable(project_key);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'add_attachment_from_public_url': {
      const issueIdOrKey: any = request.params.arguments?.issueIdOrKey;
      const imageUrl: any = request.params.arguments?.imageUrl;

      if (!issueIdOrKey || !imageUrl) {
        throw new Error('Issue id or key and image URL are required');
      }

      const response = await addAttachment(issueIdOrKey, imageUrl);

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }

    case 'add_attachment_from_confluence': {
      const issueIdOrKey: any = request.params.arguments?.issueIdOrKey;
      const pageId: any = request.params.arguments?.pageId;
      const attachmentName: any = request.params.arguments?.attachmentName;

      if (!issueIdOrKey || !pageId || !attachmentName) {
        throw new Error(
          'Issue id or key, page id and attachment name are required',
        );
      }

      const response = await addAttachmentFromConfluence(
        issueIdOrKey,
        pageId,
        attachmentName,
      );

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(response, null, 2),
          },
        ],
      };
    }
    default:
      throw new Error('Unknown tool');
  }
});

/**
 * Start the server using stdio transport.
 */
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error('Server error:', error);
  process.exit(1);
});
